package com.gxd.business.websocket;

import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.gxd.business.websocket.constant.WebsocketConst;
import com.gxd.common.constant.CacheConstants;
import com.gxd.common.core.domain.model.LoginUser;
import com.gxd.common.core.redis.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Author dhr
 * @Description: 此注解相当于设置访问URL
 */
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}/{token}")
public class WebSocketServer {

    private Session session;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 缓存 webSocket连接到单机服务class中（整体方案支持集群）
     */
    private static CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
    /**
     * 线程安全Map
     */
    private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();

    private static WebSocketServer webSocketStatic;

    @Autowired
    private RedisCache redisCache;

    @PostConstruct
    public void init(){
        webSocketStatic = this;
        webSocketStatic.redisCache = this.redisCache;
    }

//--------------------------------------------------------------------------------------------------------------------------------------------


    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId, @PathParam("token") String token) {
        try {
            log.info("【websocket消息】新连接请求，userId: {}, token: {}", userId, token);

            if (isValidToken(userId, token)) {
                this.session = session;
                this.userId = userId;
                webSockets.add(this);
                sessionPool.put(userId, session);
                log.info("【websocket消息】连接成功，userId: {}，当前连接总数: {}", userId, webSockets.size());
            } else {
                log.warn("【websocket消息】无效的Token, 拒绝连接. userId: {}, token: {}", userId, token);
                session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "无效的Token"));
            }
        } catch (Exception e) {
            log.error("【websocket消息】连接异常, userId: {}, token: {}", userId, token, e);
            try {
                session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "连接异常"));
            } catch (Exception ex) {
                log.error("【websocket消息】关闭连接异常, userId: {}", userId, ex);
            }
        }
    }

    private boolean isValidToken(String userId, String token) {
        log.info("【WebSocket】验证 Token 开始, userId: {}, token: {}", userId, token);

        if (StrUtil.isBlank(userId) || StrUtil.isBlank(token)) {
            log.warn("【WebSocket】userId 或 token 为空，验证失败");
            return false;
        }

        // 从 Redis 获取缓存的登录用户信息
        LoginUser loginUser = (LoginUser) WebSocketServer.webSocketStatic
                .redisCache.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + token);

        if (ObjUtil.isNull(loginUser)) {
            log.warn("【WebSocket】未找到对应的登录信息，验证失败，token: {}", token);
            return false;
        }

        // 从登录用户中提取 userId 字符串用于比对
        String tokenUserId = loginUser.getUserId() != null ? loginUser.getUserId().toString() : null;

        boolean isValid = userId.equals(tokenUserId);
        if (isValid) {
            log.info("【WebSocket】Token 验证成功, userId: {}", userId);
        } else {
            log.warn("【WebSocket】Token 验证失败，userId 不匹配，tokenUserId: {}", tokenUserId);
        }

        return isValid;
    }

    @OnClose
    public void onClose() {
        try {
            webSockets.remove(this);
            sessionPool.remove(this.userId);
            log.info("【websocket消息】连接断开，总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }

    @OnMessage
    public void onMessage(String message) {
        log.debug("【websocket消息】收到客户端消息:" + message);
        JSONObject obj = new JSONObject();
        //业务类型
        obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_CHECK);
        //消息内容
        obj.put(WebsocketConst.MSG_TXT, "心跳响应，自定义信息："+message);
        //update-begin-author:taoyan date:20220308 for: 消息通知长连接启动心跳机制，后端代码小bug #3473
        for (WebSocketServer webSocket : webSockets) {
            webSocket.pushMessage(obj.toJSONString());
        }
        //update-end-author:taoyan date:20220308 for: 消息通知长连接启动心跳机制，后端代码小bug #3473
    }

    /**
     * 服务器端推送消息
     */
    public void pushMessage(String message) {
        try {
            // 使用异步方式发送文本消息给对应客户端
            webSockets.forEach(ws -> ws.session.getAsyncRemote().sendText(message));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 服务端推送消息
     * 有同步锁，避免并发写入问题
     * @param userId
     * @param message
     */
    public void pushMessage(String userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null && session.isOpen()) {
            try {
                //update-begin-author:taoyan date:20211012 for: websocket报错 https://gitee.com/jeecg/jeecg-boot/issues/I4C0MU
                synchronized (session){
                    log.info("【websocket消息】 单点消息:" + message);
                    session.getBasicRemote().sendText(message);
                }
                //update-end-author:taoyan date:20211012 for: websocket报错 https://gitee.com/jeecg/jeecg-boot/issues/I4C0MU
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 格式化消息
     *
     * @param cmd
     * @param txt
     */
    public String formatMessage(String cmd, String txt) {
        JSONObject obj = new JSONObject();

        //业务类型
        obj.put(WebsocketConst.MSG_CMD, cmd);
        //消息内容
        obj.put(WebsocketConst.MSG_TXT, txt);

        return obj.toString();
    }

}